/*

This file is part of Ext JS 4

Copyright (c) 2011 Sencha Inc

Contact:  http://www.sencha.com/contact

Commercial Usage
Licensees holding valid commercial licenses may use this file in accordance with the Commercial Software License Agreement provided with the Software or, alternatively, in accordance with the terms contained in a written agreement between you and Sencha.

If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.

*/
/**
 * @class Ext.menu.Menu
 * @extends Ext.panel.Panel
 *
 * A menu object. This is the container to which you may add {@link Ext.menu.Item menu items}.
 *
 * Menus may contain either {@link Ext.menu.Item menu items}, or general {@link Ext.Component Components}.
 * Menus may also contain {@link Ext.panel.AbstractPanel#dockedItems docked items} because it extends {@link Ext.panel.Panel}.
 *
 * To make a contained general {@link Ext.Component Component} line up with other {@link Ext.menu.Item menu items},
 * specify `{@link Ext.menu.Item#iconCls iconCls}: 'no-icon'` _or_ `{@link Ext.menu.Item#indent indent}: true`.
 * This reserves a space for an icon, and indents the Component in line with the other menu items.
 * See {@link Ext.form.field.ComboBox}.{@link Ext.form.field.ComboBox#getListParent getListParent} for an example.
 *
 * By default, Menus are absolutely positioned, floating Components. By configuring a Menu with `{@link #floating}:false`,
 * a Menu may be used as a child of a {@link Ext.container.Container Container}.
 *
 * {@img Ext.menu.Item/Ext.menu.Item.png Ext.menu.Item component}
 *
 *__Example Usage__
 *
 *     Ext.create('Ext.menu.Menu', {
 *         width: 100,
 *         height: 100,
 *         margin: '0 0 10 0',
 *         floating: false,  // usually you want this set to True (default)
 *         renderTo: Ext.getBody(),  // usually rendered by it's containing component
 *         items: [{                        
 *             text: 'regular item 1'        
 *         },{
 *             text: 'regular item 2'
 *         },{
 *             text: 'regular item 3'  
 *         }]
 *     }); 
 *     
 *     Ext.create('Ext.menu.Menu', {
 *         width: 100,
 *         height: 100,
 *         plain: true,
 *         floating: false,  // usually you want this set to True (default)
 *         renderTo: Ext.getBody(),  // usually rendered by it's containing component
 *         items: [{                        
 *             text: 'plain item 1'    
 *         },{
 *             text: 'plain item 2'
 *         },{
 *             text: 'plain item 3'
 *         }]
 *     }); 
 *
 */
Ext.define('Ext.menu.Menu', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.menu',
    requires: [
        'Ext.layout.container.Fit',
        'Ext.layout.container.VBox',
        'Ext.menu.CheckItem',
        'Ext.menu.Item',
        'Ext.menu.KeyNav',
        'Ext.menu.Manager',
        'Ext.menu.Separator'
    ],

    /**
     * @cfg {Boolean} allowOtherMenus
     * True to allow multiple menus to be displayed at the same time. Defaults to `false`.
     * @markdown
     */
    allowOtherMenus: false,

    /**
     * @cfg {String} ariaRole @hide
     */
    ariaRole: 'menu',

    /**
     * @cfg {Boolean} autoRender @hide
     * floating is true, so autoRender always happens
     */

    /**
     * @cfg {String} defaultAlign
     * The default {@link Ext.core.Element#getAlignToXY Ext.core.Element#getAlignToXY} anchor position value for this menu
     * relative to its element of origin. Defaults to `'tl-bl?'`.
     * @markdown
     */
    defaultAlign: 'tl-bl?',

    /**
     * @cfg {Boolean} floating
     * A Menu configured as `floating: true` (the default) will be rendered as an absolutely positioned,
     * {@link Ext.Component#floating floating} {@link Ext.Component Component}. If configured as `floating: false`, the Menu may be
     * used as a child item of another {@link Ext.container.Container Container}.
     * @markdown
     */
    floating: true,

    /**
     * @cfg {Boolean} @hide
     * Menus are constrained to the document body by default
     */
    constrain: true,

    /**
     * @cfg {Boolean} hidden
     * True to initially render the Menu as hidden, requiring to be shown manually.
     * Defaults to `true` when `floating: true`, and defaults to `false` when `floating: false`.
     * @markdown
     */
    hidden: true,

    hideMode: 'visibility',

    /**
     * @cfg {Boolean} ignoreParentClicks
     * True to ignore clicks on any item in this menu that is a parent item (displays a submenu)
     * so that the submenu is not dismissed when clicking the parent item. Defaults to `false`.
     * @markdown
     */
    ignoreParentClicks: false,

    isMenu: true,

    /**
     * @cfg {String/Object} layout @hide
     */

    /**
     * @cfg {Boolean} showSeparator True to show the icon separator. (defaults to true).
     */
    showSeparator : true,

    /**
     * @cfg {Number} minWidth
     * The minimum width of the Menu. Defaults to `120`.
     * @markdown
     */
    minWidth: 120,

    /**
     * @cfg {Boolean} plain
     * True to remove the incised line down the left side of the menu and to not
     * indent general Component items. Defaults to `false`.
     * @markdown
     */

    initComponent: function() {
        var me = this,
            prefix = Ext.baseCSSPrefix,
            cls = [prefix + 'menu'],
            bodyCls = me.bodyCls ? [me.bodyCls] : [];

        me.addEvents(
            /**
             * @event click
             * Fires when this menu is clicked
             * @param {Ext.menu.Menu} menu The menu which has been clicked
             * @param {Ext.Component} item The menu item that was clicked. `undefined` if not applicable.
             * @param {Ext.EventObject} e The underlying {@link Ext.EventObject}.
             * @markdown
             */
            'click',

            /**
             * @event mouseenter
             * Fires when the mouse enters this menu
             * @param {Ext.menu.Menu} menu The menu
             * @param {Ext.EventObject} e The underlying {@link Ext.EventObject}
             * @markdown
             */
            'mouseenter',

            /**
             * @event mouseleave
             * Fires when the mouse leaves this menu
             * @param {Ext.menu.Menu} menu The menu
             * @param {Ext.EventObject} e The underlying {@link Ext.EventObject}
             * @markdown
             */
            'mouseleave',

            /**
             * @event mouseover
             * Fires when the mouse is hovering over this menu
             * @param {Ext.menu.Menu} menu The menu
             * @param {Ext.Component} item The menu item that the mouse is over. `undefined` if not applicable.
             * @param {Ext.EventObject} e The underlying {@link Ext.EventObject}
             */
            'mouseover'
        );

        Ext.menu.Manager.register(me);

        // Menu classes
        if (me.plain) {
            cls.push(prefix + 'menu-plain');
        }
        me.cls = cls.join(' ');

        // Menu body classes
        bodyCls.unshift(prefix + 'menu-body');
        me.bodyCls = bodyCls.join(' ');

        // Internal vbox layout, with scrolling overflow
        // Placed in initComponent (rather than prototype) in order to support dynamic layout/scroller
        // options if we wish to allow for such configurations on the Menu.
        // e.g., scrolling speed, vbox align stretch, etc.
        me.layout = {
            type: 'vbox',
            align: 'stretchmax',
            autoSize: true,
            clearInnerCtOnLayout: true,
            overflowHandler: 'Scroller'
        };

        // hidden defaults to false if floating is configured as false
        if (me.floating === false && me.initialConfig.hidden !== true) {
            me.hidden = false;
        }

        me.callParent(arguments);

        me.on('beforeshow', function() {
            var hasItems = !!me.items.length;
            // FIXME: When a menu has its show cancelled because of no items, it
            // gets a visibility: hidden applied to it (instead of the default display: none)
            // Not sure why, but we remove this style when we want to show again.
            if (hasItems && me.rendered) {
                me.el.setStyle('visibility', null);
            }
            return hasItems;
        });
    },

    afterRender: function(ct) {
        var me = this,
            prefix = Ext.baseCSSPrefix,
            space = '&#160;';

        me.callParent(arguments);

        // TODO: Move this to a subTemplate When we support them in the future
        if (me.showSeparator) {
            me.iconSepEl = me.layout.getRenderTarget().insertFirst({
                cls: prefix + 'menu-icon-separator',
                html: space
            });
        }

        me.focusEl = me.el.createChild({
            cls: prefix + 'menu-focus',
            tabIndex: '-1',
            html: space
        });

        me.mon(me.el, {
            click: me.onClick,
            mouseover: me.onMouseOver,
            scope: me
        });
        me.mouseMonitor = me.el.monitorMouseLeave(100, me.onMouseLeave, me);

        if (me.showSeparator && ((!Ext.isStrict && Ext.isIE) || Ext.isIE6)) {
            me.iconSepEl.setHeight(me.el.getHeight());
        }

        me.keyNav = Ext.create('Ext.menu.KeyNav', me);
    },

    afterLayout: function() {
        var me = this;
        me.callParent(arguments);

        // For IE6 & IE quirks, we have to resize the el and body since position: absolute
        // floating elements inherit their parent's width, making them the width of
        // document.body instead of the width of their contents.
        // This includes left/right dock items.
        if ((!Ext.iStrict && Ext.isIE) || Ext.isIE6) {
            var innerCt = me.layout.getRenderTarget(),
                innerCtWidth = 0,
                dis = me.dockedItems,
                l = dis.length,
                i = 0,
                di, clone, newWidth;

            innerCtWidth = innerCt.getWidth();

            newWidth = innerCtWidth + me.body.getBorderWidth('lr') + me.body.getPadding('lr');

            // First set the body to the new width
            me.body.setWidth(newWidth);

            // Now we calculate additional width (docked items) and set the el's width
            for (; i < l, di = dis.getAt(i); i++) {
                if (di.dock == 'left' || di.dock == 'right') {
                    newWidth += di.getWidth();
                }
            }
            me.el.setWidth(newWidth);
        }
    },

    /**
     * Returns whether a menu item can be activated or not.
     * @return {Boolean}
     */
    canActivateItem: function(item) {
        return item && !item.isDisabled() && item.isVisible() && (item.canActivate || item.getXTypes().indexOf('menuitem') < 0);
    },

    /**
     * Deactivates the current active item on the menu, if one exists.
     */
    deactivateActiveItem: function() {
        var me = this;

        if (me.activeItem) {
            me.activeItem.deactivate();
            if (!me.activeItem.activated) {
                delete me.activeItem;
            }
        }
        if (me.focusedItem) {
            me.focusedItem.blur();
            if (!me.focusedItem.$focused) {
                delete me.focusedItem;
            }
        }
    },

    clearStretch: function () {
        // the vbox/stretchmax will set the el sizes and subsequent layouts will not
        // reconsider them unless we clear the dimensions on the el's here:
        if (this.rendered) {
            this.items.each(function (item) {
                // each menuItem component needs to layout again, so clear its cache
                if (item.componentLayout) {
                    delete item.componentLayout.lastComponentSize;
                }
                if (item.el) {
                    item.el.setWidth(null);
                }
            });
        }
    },

    onAdd: function () {
        var me = this;

        me.clearStretch();
        me.callParent(arguments);

        if (Ext.isIE6 || Ext.isIE7) {
            // TODO - why does this need to be done (and not ok to do now)?
            Ext.Function.defer(me.doComponentLayout, 10, me);
        }
    },

    onRemove: function () {
        this.clearStretch();
        this.callParent(arguments);

    },

    redoComponentLayout: function () {
        if (this.rendered) {
            this.clearStretch();
            this.doComponentLayout();
        }
    },

    // inherit docs
    getFocusEl: function() {
        return this.focusEl;
    },

    // inherit docs
    hide: function() {
        this.deactivateActiveItem();
        this.callParent(arguments);
    },

    // private
    getItemFromEvent: function(e) {
        return this.getChildByElement(e.getTarget());
    },

    lookupComponent: function(cmp) {
        var me = this;

        if (Ext.isString(cmp)) {
            cmp = me.lookupItemFromString(cmp);
        } else if (Ext.isObject(cmp)) {
            cmp = me.lookupItemFromObject(cmp);
        }

        // Apply our minWidth to all of our child components so it's accounted
        // for in our VBox layout
        cmp.minWidth = cmp.minWidth || me.minWidth;

        return cmp;
    },

    // private
    lookupItemFromObject: function(cmp) {
        var me = this,
            prefix = Ext.baseCSSPrefix,
            cls,
            intercept;

        if (!cmp.isComponent) {
            if (!cmp.xtype) {
                cmp = Ext.create('Ext.menu.' + (Ext.isBoolean(cmp.checked) ? 'Check': '') + 'Item', cmp);
            } else {
                cmp = Ext.ComponentManager.create(cmp, cmp.xtype);
            }
        }

        if (cmp.isMenuItem) {
            cmp.parentMenu = me;
        }

        if (!cmp.isMenuItem && !cmp.dock) {
            cls = [prefix + 'menu-item', prefix + 'menu-item-cmp'];
            intercept = Ext.Function.createInterceptor;

            // Wrap focus/blur to control component focus
            cmp.focus = intercept(cmp.focus, function() {
                this.$focused = true;
            }, cmp);
            cmp.blur = intercept(cmp.blur, function() {
                this.$focused = false;
            }, cmp);

            if (!me.plain && (cmp.indent === true || cmp.iconCls === 'no-icon')) {
                cls.push(prefix + 'menu-item-indent');
            }

            if (cmp.rendered) {
                cmp.el.addCls(cls);
            } else {
                cmp.cls = (cmp.cls ? cmp.cls : '') + ' ' + cls.join(' ');
            }
            cmp.isMenuItem = true;
        }
        return cmp;
    },

    // private
    lookupItemFromString: function(cmp) {
        return (cmp == 'separator' || cmp == '-') ?
            Ext.createWidget('menuseparator')
            : Ext.createWidget('menuitem', {
                canActivate: false,
                hideOnClick: false,
                plain: true,
                text: cmp
            });
    },

    onClick: function(e) {
        var me = this,
            item;

        if (me.disabled) {
            e.stopEvent();
            return;
        }

        if ((e.getTarget() == me.focusEl.dom) || e.within(me.layout.getRenderTarget())) {
            item = me.getItemFromEvent(e) || me.activeItem;

            if (item) {
                if (item.getXTypes().indexOf('menuitem') >= 0) {
                    if (!item.menu || !me.ignoreParentClicks) {
                        item.onClick(e);
                    } else {
                        e.stopEvent();
                    }
                }
            }
            me.fireEvent('click', me, item, e);
        }
    },

    onDestroy: function() {
        var me = this;

        Ext.menu.Manager.unregister(me);
        if (me.rendered) {
            me.el.un(me.mouseMonitor);
            me.keyNav.destroy();
            delete me.keyNav;
        }
        me.callParent(arguments);
    },

    onMouseLeave: function(e) {
        var me = this;

        me.deactivateActiveItem();

        if (me.disabled) {
            return;
        }

        me.fireEvent('mouseleave', me, e);
    },

    onMouseOver: function(e) {
        var me = this,
            fromEl = e.getRelatedTarget(),
            mouseEnter = !me.el.contains(fromEl),
            item = me.getItemFromEvent(e);

        if (mouseEnter && me.parentMenu) {
            me.parentMenu.setActiveItem(me.parentItem);
            me.parentMenu.mouseMonitor.mouseenter();
        }

        if (me.disabled) {
            return;
        }

        if (item) {
            me.setActiveItem(item);
            if (item.activated && item.expandMenu) {
                item.expandMenu();
            }
        }
        if (mouseEnter) {
            me.fireEvent('mouseenter', me, e);
        }
        me.fireEvent('mouseover', me, item, e);
    },

    setActiveItem: function(item) {
        var me = this;

        if (item && (item != me.activeItem && item != me.focusedItem)) {
            me.deactivateActiveItem();
            if (me.canActivateItem(item)) {
                if (item.activate) {
                    item.activate();
                    if (item.activated) {
                        me.activeItem = item;
                        me.focusedItem = item;
                        me.focus();
                    }
                } else {
                    item.focus();
                    me.focusedItem = item;
                }
            }
            item.el.scrollIntoView(me.layout.getRenderTarget());
        }
    },

    /**
     * Shows the floating menu by the specified {@link Ext.Component Component} or {@link Ext.core.Element Element}.
     * @param {Mixed component} The {@link Ext.Component} or {@link Ext.core.Element} to show the menu by.
     * @param {String} position (optional) Alignment position as used by {@link Ext.core.Element#getAlignToXY Ext.core.Element.getAlignToXY}. Defaults to `{@link #defaultAlign}`.
     * @param {Array} offsets (optional) Alignment offsets as used by {@link Ext.core.Element#getAlignToXY Ext.core.Element.getAlignToXY}. Defaults to `undefined`.
     * @return {Menu} This Menu.
     * @markdown
     */
    showBy: function(cmp, pos, off) {
        var me = this,
            xy,
            region;

        if (me.floating && cmp) {
            me.layout.autoSize = true;

            // show off-screen first so that we can calc position without causing a visual jump
            me.doAutoRender();

            // Component or Element
            cmp = cmp.el || cmp;

            // Convert absolute to floatParent-relative coordinates if necessary.
            xy = me.el.getAlignToXY(cmp, pos || me.defaultAlign, off);
            if (me.floatParent) {
                region = me.floatParent.getTargetEl().getViewRegion();
                xy[0] -= region.x;
                xy[1] -= region.y;
            }
            me.showAt(xy);
        }
        return me;
    },
    
    // inherit docs
    showAt: function(){
        this.callParent(arguments);
        if (this.floating) {
            this.doConstrain();
        }    
    },

    doConstrain : function() {
        var me = this,
            y = me.el.getY(),
            max, full,
            vector,
            returnY = y, normalY, parentEl, scrollTop, viewHeight;

        delete me.height;
        me.setSize();
        full = me.getHeight();
        if (me.floating) {
            parentEl = Ext.fly(me.el.dom.parentNode);
            scrollTop = parentEl.getScroll().top;
            viewHeight = parentEl.getViewSize().height;
            //Normalize y by the scroll position for the parent element.  Need to move it into the coordinate space
            //of the view.
            normalY = y - scrollTop;
            max = me.maxHeight ? me.maxHeight : viewHeight - normalY;
            if (full > viewHeight) {
                max = viewHeight;
                //Set returnY equal to (0,0) in view space by reducing y by the value of normalY
                returnY = y - normalY;
            } else if (max < full) {
                returnY = y - (full - max);
                max = full;
            }
        }else{
            max = me.getHeight();
        }
        // Always respect maxHeight
        if (me.maxHeight){
            max = Math.min(me.maxHeight, max);
        }
        if (full > max && max > 0){
            me.layout.autoSize = false;
            me.setHeight(max);
            if (me.showSeparator){
                me.iconSepEl.setHeight(me.layout.getRenderTarget().dom.scrollHeight);
            }
        }
        vector = me.getConstrainVector(me.el.dom.parentNode);
        if (vector) {
            me.setPosition(me.getPosition()[0] + vector[0]);
        }
        me.el.setY(returnY);
    }
});
